Let an application authenticate the sign-in it is told about - #101
Conversation
The invitation issuer already minted RS256 envelopes with key rotation, issuer and audience binding, a lifetime and a random identifier, but it did so behind a private method, so a second caller had no way to reach it without duplicating the signing or the configuration checks that guard it. Lifting it out is deliberate: one signing implementation is one place to audit, one place where a key is loaded, and one place a mistake can live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
The sign-in notification was posted as unsigned JSON, so anything that could reach that endpoint chose which user the application recorded as having signed in, including the subject and the identity provider. The notification can now carry an envelope binding six facts: who signed it, which application it is for, the method and target it was sent to, a digest of the exact bytes posted, when it was issued, and a single-use identifier. Route and body use the RFC 9449 claim names so this is a profile of an existing scheme rather than a private one. The digest is taken over the serialized bytes rather than the object they came from, because only the former is what the verifier will actually see. It is opt-in and gated on configuration alone, so a deployment that does not configure it keeps today's behavior byte for byte. When it is configured and an envelope cannot be issued, nothing is posted at all -- a notification that cannot be authenticated is worth less than none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
|
Reviewer context, kept out of the body. Intent was posted on #82 before any code was written; this implements that shape. Why the envelope reuses the invitation stackThe invitation attestation issuer already minted RS256 envelopes with The two additions are Two deliberate deviations from RFC 9449, both worth a reviewer's attention:
A CompatibilityGated on the presence of A real defect the implementation pass found and fixedConvention-based DI resolves the greediest satisfiable constructor. Independently re-verified before mergeI ran the security-critical mutation myself rather than accepting the report. Rewriting the fail-closed branch to fail open — post unsigned when the envelope cannot be issued — fails 4 assertions, and importantly not just the result code: The one assertion that cannot be made to fail — stated, not hiddenThe digest is taken over An attempt to close it by pinning that the content had been buffered was reverted — Similarly, a hard-coded Prerequisite the owner should decide separatelyAuthProxy exposes no JWKS endpoint. A verifying application pins public keys by configuration and selects by Deliberately out of scopeThe invite-exchange and credential-link back-channels post similar payloads; the invite arm already carries an attestation but binds neither route nor body. Both are now one call on GatesDebug and Release, both Not verifiedNo real application verified a real envelope end to end — verification is exercised by a spec-side verifier built from the published contract, not by a separate implementation. No key rotation was performed against a live deployment. |
Every constant in the published envelope -- the claim names and the value that separates a sign-in notification from an invitation attestation -- appeared in exactly one file, and every assertion read that constant and compared it against itself. Renaming the separating value to the invitation's left the whole suite green while every deployed verifier broke and the separation the documentation promises collapsed. The contract is now pinned as literals, and asserted to differ from both invitation purposes. The duplicate-key guard was live but unpinned for the case that matters: the only spec duplicated the active key, so it died to the active-key rule rather than to uniqueness, and the guard could have been deleted unnoticed. Rotation to a duplicated identifier would then have thrown out of the signer and broken the sign-in it was only supposed to record -- so the lookup now takes the first match rather than demanding a single one, and fails closed if configuration ever lets one through. An undersized RSA key signed happily, because the identity library does not refuse one and the signer is reachable without the configuration validator. A signing endpoint carrying a query was accepted while the route binding deliberately excludes the query, leaving a replay window to a different query string. The signing contract's generated ToString printed the private key, and a key identifier of null crashed the configuration check instead of reporting it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
|
Hardening pass — addresses both independent reviews. The finding that mattered mostEvery constant in the published envelope — Verified independently before and after. Renaming One review claim was refuted, and I checked that myselfThe code review reported the duplicate-key guard as only catching a duplicate of the active key, with What was true is the coverage half: mutating The Everything else applied
Mutation evidence22 mutations, each applied, rebuilt and re-run against the full suite — not reasoned. All 32 new assertions confirmed load-bearing, each dying to a single named line. Every negative is paired with a positive baseline dying to the opposite mutation of the same line (e.g. the key-size floor: Still unfalsifiable, unchanged and still disclosedThe body digest over serialized bytes versus the payload object remains unkillable — both emit identical bytes while the payload is camelCase. The guarantee is structural and matters the moment a payload's serializer options diverge. GatesDebug and Release, both Deliberately not doneCaching |
Added
SignIn:Attestationconfiguration, with rotating signing keys selected by key identifier, enabling the signed envelope (Offer one authenticated AuthProxy-to-application request envelope #82)Security